<?php

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *	   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package log4php
 */

/**
 * Most of the work of the {@link LoggerPatternLayout} class
 * is delegated to the {@link LoggerPatternParser} class.
 *
 * <p>It is this class that parses conversion patterns and creates
 * a chained list of {@link LoggerPatternConverter} converters.</p>
 *
 * @version $Revision: 1395467 $
 * @package log4php
 * @subpackage helpers
 *            
 * @since 0.3
 */
class LoggerPatternParser
{

    /**
     * Escape character for conversion words in the conversion pattern.
     */
    const ESCAPE_CHAR = '%';

    /**
     * Maps conversion words to relevant converters.
     */
    private $converterMap;

    /**
     * Conversion pattern used in layout.
     */
    private $pattern;

    /**
     * Regex pattern used for parsing the conversion pattern.
     */
    private $regex;

    /**
     * First converter in the chain.
     *
     * @var LoggerPatternConverter
     */
    private $head;

    /**
     * Last converter in the chain.
     */
    private $tail;

    public function __construct($pattern, $converterMap)
    {
        $this->pattern = $pattern;
        $this->converterMap = $converterMap;
        
        // Construct the regex pattern
        $this->regex = '/' .         // Starting regex pattern delimiter
        self::ESCAPE_CHAR .         // Character which marks the start of the conversion
                            // pattern
        '(?P<modifiers>[0-9.-]*)' .         // Format modifiers (optional)
        '(?P<word>[a-zA-Z]+)' .         // The conversion word
        '(?P<option>{[^}]*})?' .         // Conversion option in braces (optional)
        '/'; // Ending regex pattern delimiter
    }

    /**
     * Parses the conversion pattern string, converts it to a chain of pattern
     * converters and returns the first converter in the chain.
     *
     * @return LoggerPatternConverter
     */
    public function parse()
    {
        
        // Skip parsing if the pattern is empty
        if (empty($this->pattern)) {
            $this->addLiteral('');
            return $this->head;
        }
        
        // Find all conversion words in the conversion pattern
        $count = preg_match_all($this->regex, $this->pattern, $matches, PREG_OFFSET_CAPTURE);
        if ($count === false) {
            $error = error_get_last();
            throw new LoggerException("Failed parsing layotut pattern: {$error['message']}");
        }
        
        $prevEnd = 0;
        
        foreach ($matches[0] as $key => $item) {
            
            // Locate where the conversion command starts and ends
            $length = strlen($item[0]);
            $start = $item[1];
            $end = $item[1] + $length;
            
            // Find any literal expressions between matched commands
            if ($start > $prevEnd) {
                $literal = substr($this->pattern, $prevEnd, $start - $prevEnd);
                $this->addLiteral($literal);
            }
            
            // Extract the data from the matched command
            $word = ! empty($matches['word'][$key]) ? $matches['word'][$key][0] : null;
            $modifiers = ! empty($matches['modifiers'][$key]) ? $matches['modifiers'][$key][0] : null;
            $option = ! empty($matches['option'][$key]) ? $matches['option'][$key][0] : null;
            
            // Create a converter and add it to the chain
            $this->addConverter($word, $modifiers, $option);
            
            $prevEnd = $end;
        }
        
        // Add any trailing literals
        if ($end < strlen($this->pattern)) {
            $literal = substr($this->pattern, $end);
            $this->addLiteral($literal);
        }
        
        return $this->head;
    }

    /**
     * Adds a literal converter to the converter chain.
     *
     * @param string $string
     *            The string for the literal converter.
     */
    private function addLiteral($string)
    {
        $converter = new LoggerPatternConverterLiteral($string);
        $this->addToChain($converter);
    }

    /**
     * Adds a non-literal converter to the converter chain.
     *
     * @param string $word
     *            The conversion word, used to determine which
     *            converter will be used.
     * @param string $modifiers
     *            Formatting modifiers.
     * @param string $option
     *            Option to pass to the converter.
     */
    private function addConverter($word, $modifiers, $option)
    {
        $formattingInfo = $this->parseModifiers($modifiers);
        $option = trim($option, "{} ");
        
        if (isset($this->converterMap[$word])) {
            $converter = $this->getConverter($word, $formattingInfo, $option);
            $this->addToChain($converter);
        } else {
            trigger_error("log4php: Invalid keyword '%$word' in converison pattern. Ignoring keyword.", E_USER_WARNING);
        }
    }

    /**
     * Determines which converter to use based on the conversion word.
     * Creates
     * an instance of the converter using the provided formatting info and
     * option and returns it.
     *
     * @param string $word
     *            The conversion word.
     * @param LoggerFormattingInfo $info
     *            Formatting info.
     * @param string $option
     *            Converter option.
     *            
     * @throws LoggerException
     *
     * @return LoggerPatternConverter
     */
    private function getConverter($word, $info, $option)
    {
        if (! isset($this->converterMap[$word])) {
            throw new LoggerException("Invalid keyword '%$word' in converison pattern. Ignoring keyword.");
        }
        
        $converterClass = $this->converterMap[$word];
        if (! class_exists($converterClass)) {
            throw new LoggerException("Class '$converterClass' does not exist.");
        }
        
        $converter = new $converterClass($info, $option);
        if (! ($converter instanceof LoggerPatternConverter)) {
            throw new LoggerException("Class '$converterClass' is not an instance of LoggerPatternConverter.");
        }
        
        return $converter;
    }

    /**
     * Adds a converter to the chain and updates $head and $tail pointers.
     */
    private function addToChain(LoggerPatternConverter $converter)
    {
        if (! isset($this->head)) {
            $this->head = $converter;
            $this->tail = $this->head;
        } else {
            $this->tail->next = $converter;
            $this->tail = $this->tail->next;
        }
    }

    /**
     * Parses the formatting modifiers and produces the corresponding
     * LoggerFormattingInfo object.
     *
     * @param string $modifier            
     * @return LoggerFormattingInfo
     * @throws LoggerException
     */
    private function parseModifiers($modifiers)
    {
        $info = new LoggerFormattingInfo();
        
        // If no modifiers are given, return default values
        if (empty($modifiers)) {
            return $info;
        }
        
        // Validate
        $pattern = '/^(-?[0-9]+)?\.?-?[0-9]+$/';
        if (! preg_match($pattern, $modifiers)) {
            trigger_error("log4php: Invalid modifier in conversion pattern: [$modifiers]. Ignoring modifier.", E_USER_WARNING);
            return $info;
        }
        
        $parts = explode('.', $modifiers);
        
        if (! empty($parts[0])) {
            $minPart = (integer) $parts[0];
            $info->min = abs($minPart);
            $info->padLeft = ($minPart > 0);
        }
        
        if (! empty($parts[1])) {
            $maxPart = (integer) $parts[1];
            $info->max = abs($maxPart);
            $info->trimLeft = ($maxPart < 0);
        }
        
        return $info;
    }
}
